https://cryptohack.org/courses/symmetric/ecb_oracle/
這邊要先提一下 ECB 模式的弱點,之前的 ECB 小介紹有提到:
在ECB模式中每個明文塊都會單獨加密,產生相同長度的密文塊。相同的明文塊所產生的密文塊會完全相同。
因此,這種模式無法隱藏數據的結構或重複性,導致容易被分析。
所以可以這樣解密:逐字節測試不同的明文,觀察密文的變化,從而推測出原始明文。
而我們收到的密文是怎麼產生的呢?
我們輸入的明文與 FLAG 串接後的字串會再使用 pad 函數來填充,使其長度成為AES的區塊大小(16 bytes)的倍數,最後再進行AES加密。
所以解密的過程是這樣的:
上一篇已經提到可以透過request的方法向網頁端發送請求,來獲得加密後的密文。
而這部分就是向網頁發送加密的請求,並獲得密文的程式碼。
import requests
def encrypted(plaintext):
url = "https://aes.cryptohack.org/ecb_oracle/encrypt/"
hex_plaintext = plaintext.encode().hex() # 將字串轉換為hex,因為網站的加解密輸出與輸入都要是hex格式
response = requests.get(url + hex_plaintext) # 發送GET請求並獲取響應
ciphertext = response.text[15:-3]
# 提取密文,因為網站會回傳{"ciphertext":"53d03b61cae58e77b7cc33570b01ff9a022c13d750f4cb2f24e68c73ebb7a4d2"}這樣格式的內容,而我們只需要:後的密文就好,所以用[15:-3]來擷取第15個字元開始到倒數第3個字元
return ciphertext
上上一篇文章有提到,flag總共有25個字元,而現在我們要找出前16個(1 block)。
def find_flag_1():
flag = "crypto{" # 已知的前7個字元
# 第一個區塊
# 從第8個字元開始猜測,直到第16個字元(第一個區塊的最後一個字元)
for i in range(7, 16):
# 調整填充用的明文長度,使得待猜測的字元恰好位於區塊的最後一個位置
plain = "a" * (15 - i)
# 獲取只包含填充的密文(僅第一個區塊 1 block = 16 bytes = 32個hex字符)
cipher = encrypted(plain)[:32]
# 遍歷所有可能的ASCII可打印字符(從33到127)
for test in range(33, 128):
# 建構待測試的明文:填充用的明文 + 已知flag + 猜測字符
plain_try = plain + flag + chr(test)
# 加密待測試的明文並得到第一個區塊
cipher_try = encrypted(plain_try)[:32]
# 如果密文匹配,則找到了正確的字符
if cipher == cipher_try:
# 將找到的字符添加到flag中
flag += chr(test)
print(f"找到字符: {flag}")
break # 找到字符後跳出內層循環,繼續猜測下一個字符
return flag
找出剩餘的flag字元
因為flag的最後一個字元是"}",所以為了省時間,只要找出第17-24個字符,最後返回的時候再加上"}"就好。
def find_flag_2(flag):
# 第二個區塊的解密
print("進入到第二階段")
for i in range(len(flag), 24): # 從第17個字符開始猜測,直到第24個字符
# 調整填充長度,使待猜測的字符位於第二個區塊的最後一個位置
plain = "a" * (31 - i)
# 獲取只包含填充的密文的第二個區塊
cipher = encrypted(plain)[32:64]
# 遍歷所有可能的ASCII可打印字符(從33到127)
for test in range(33, 128):
# 構造待測試的明文:填充用的明文 + 已知flag + 猜測字符
plain_try = plain + flag + chr(test)
# 加密待測試的明文並獲取第二個區塊
cipher_try = encrypted(plain_try)[32:64]
# 如果密文匹配,則找到了正確的字符
if cipher == cipher_try:
# 將找到的字符添加到flag中
flag += chr(test)
print(f"找到字符: {flag}")
break # 找到字符後跳出內層循環,繼續猜測下一個字符
# 返回完整的flag,包括結尾的 "}"
return flag + "}"
完整code:
import requests
def encrypted(plaintext):
url = "https://aes.cryptohack.org/ecb_oracle/encrypt/"
hex_plaintext = plaintext.encode().hex() # 將字串轉換為十六進位表示
response = requests.get(url + hex_plaintext) # 發送GET請求並獲取響應
ciphertext = response.text[15:-3] # 提取密文
return ciphertext
def find_flag_1():
flag = "crypto{" # 已知的前7個字元
# 第一個區塊
# 從第8個字元開始猜測,直到第16個字元(第一個區塊的最後一個字元)
for i in range(7, 16):
# 調整填充長度,使得待猜測的字元恰好位於區塊的最後一個位置
plain = "a" * (15 - i)
# 獲取只包含填充的密文(僅第一個區塊)
cipher = encrypted(plain)[:32]
# 遍歷所有可能的ASCII可打印字符(從33到127)
for test in range(33, 128):
# 構造待測試的明文:填充 + 已知flag + 猜測字符
plain_try = plain + flag + chr(test)
# 加密待測試的明文並獲取第一個區塊
cipher_try = encrypted(plain_try)[:32]
# 如果密文匹配,則找到了正確的字符
if cipher == cipher_try:
# 將找到的字符添加到flag中
flag += chr(test)
print(f"找到字符: {flag}")
break # 找到字符後跳出內層循環,繼續猜測下一個字符
# 返回找到的flag(不包括最後的 "}")
return flag
def find_flag_2(flag):
# 第二個區塊的解密
print("進入到第二階段")
for i in range(len(flag), 24): # 從第17個字符開始猜測,直到第24個字符
# 調整填充長度,使待猜測的字符位於第二個區塊的最後一個位置
plain = "a" * (31 - i)
# 獲取只包含填充的密文的第二個區塊
cipher = encrypted(plain)[32:64]
# 遍歷所有可能的ASCII可打印字符(從33到127)
for test in range(33, 128):
# 構造待測試的明文:填充 + 已知flag + 猜測字符
plain_try = plain + flag + chr(test)
# 加密待測試的明文並獲取第二個區塊
cipher_try = encrypted(plain_try)[32:64]
# 如果密文匹配,則找到了正確的字符
if cipher == cipher_try:
# 將找到的字符添加到flag中
flag += chr(test)
print(f"找到字符: {flag}")
break # 找到字符後跳出內層循環,繼續猜測下一個字符
# 返回完整的flag,包括結尾的 "}"
return flag + "}"
if __name__ == "__main__":
result = find_flag_2(find_flag_1())
if result:
print(f"完整的 flag 是: {result}")
else:
print("無法完成 flag 的解密"),
crypto{p3n6u1n5_h473_3cb}
penguins_hate_ecb(可能與linux企鵝圖經過ECB加密後仍然很清楚有關)
Ascii table: https://www.rapidtables.com/code/text/ascii-table.html
前人的文章: https://ithelp.ithome.com.tw/m/articles/10333803
當初debug了很久,然後又因為晚了幾分鐘發文,所以連更記錄沒了qq。
今天要來把之前還沒補完的內容補全。
備註:因為這題的程式碼要用暴力猜解法一個個去試,所以歷時較久,要很有耐心。
當初因為他跑了很久都沒有動靜想說是不是程式碼有什麼bug,所以我讓他在每一個階段只要找到flag的字符就要print出來,後來發現程式碼沒什麼問題,只是跑得太慢了。